import {
  FileInterceptor,
  FilesInterceptor,
  FileFieldsInterceptor,
} from '@nestjs/platform-express';
import { diskStorage } from 'multer';
import { extname } from 'path';
import { v1 as uuidv1 } from 'uuid';
import * as fs from 'fs';
import * as crypto from 'crypto';
import * as ffmpeg from 'fluent-ffmpeg';
import * as sharp from 'sharp';
import { Logger } from '@nestjs/common';
import { MulterField } from '@nestjs/platform-express/multer/interfaces/multer-options.interface';
import { Request } from 'express';
import { FileStoragePath } from '@/enum';

type FileTypeSupported = 'image' | 'audio' | 'video' | 'pdf';

export class FileUtils {
  private static uuidFilename(
    req: Request,
    file: Express.Multer.File,
    callback: (error: Error | null, filename: string) => void,
  ): void {
    const randomName = uuidv1();
    callback(null, `${randomName}${extname(file.originalname)}`);
  }

  private static localStorage(destination: string) {
    return diskStorage({
      destination,
      filename: this.uuidFilename,
    });
  }

  static checkFileType(
    allowFileTypes: FileTypeSupported[],
    fileMimetype: string,
  ): boolean {
    const fileType = this.getTypeFromMimetype(fileMimetype);

    return allowFileTypes.includes(fileType);
  }

  static getTypeFromMimetype(mimetype: string): FileTypeSupported {
    switch (mimetype) {
      // image
      case 'image/jpeg':
      case 'image/png':
      case 'image/gif':
      case 'image/svg+xml':
        return 'image';
      // audio
      case 'audio/aac':
      case 'audio/x-aac':
      case 'audio/mp3':
      case 'audio/mpeg':
      case 'audio/ogg':
      case 'audio/webm':
        return 'audio';
      // video
      case 'video/mp4':
      case 'video/ogg':
      case 'video/webm':
        return 'video';
      case 'application/pdf':
        return 'pdf';
    }
  }

  static singleFileUploader(
    fieldName: string,
    allowFileTypes?: FileTypeSupported[],
    destination: FileStoragePath = FileStoragePath.TMP,
    maxFileSize = 512 * 1024 * 1024,
  ) {
    return FileInterceptor(fieldName, {
      storage: this.localStorage(destination),
      fileFilter: (req: Request, file, cb) => {
        console.log(req.headers);
        console.log(req.body);
        if (
          allowFileTypes &&
          !this.checkFileType(allowFileTypes, file.mimetype)
        ) {
          cb(
            new Error(
              `Unexpected file type "${file.mimetype}" of file ${file.originalname}`,
            ),
            false,
          );
        }
        cb(null, true);
      },
      limits: { fileSize: maxFileSize },
    });
  }

  static multipleFileUploader(
    fieldName: string,
    allowFileTypes?: FileTypeSupported[],
    destination: FileStoragePath = FileStoragePath.TMP,
    maxFileNumber = 20,
    maxFileSize = 512 * 1024 * 1024,
  ) {
    return FilesInterceptor(fieldName, maxFileNumber, {
      storage: this.localStorage(destination),
      fileFilter: (req: Request, file, cb) => {
        console.log(req.headers);
        console.log(req.body);
        if (
          allowFileTypes &&
          !this.checkFileType(allowFileTypes, file.mimetype)
        ) {
          cb(
            new Error(
              `Unexpected file type "${file.mimetype}" of file ${file.originalname}`,
            ),
            false,
          );
        }
        cb(null, true);
      },
      limits: { fileSize: maxFileSize },
    });
  }

  static multipleFileFieldsUploader(
    uploadFields: MulterField[],
    allowFileTypes?: FileTypeSupported[],
    destination: FileStoragePath = FileStoragePath.TMP,
    maxFileSize = 512 * 1024 * 1024,
  ) {
    return FileFieldsInterceptor(uploadFields, {
      storage: this.localStorage(destination),
      fileFilter: (req, file, cb) => {
        if (
          allowFileTypes &&
          !this.checkFileType(allowFileTypes, file.mimetype)
        ) {
          cb(
            new Error(
              `Unexpected file type "${file.mimetype}" of file ${file.originalname}`,
            ),
            false,
          );
        }
        cb(null, true);
      },
      limits: { fileSize: maxFileSize },
    });
  }

  static async getHashOfFile(
    filePath: string,
    algorithm = 'sha256',
  ): Promise<string> {
    const hash = crypto.createHash(algorithm);
    const input = fs.createReadStream(filePath);
    return new Promise<string>((resolve, reject) => {
      input
        .on('data', chunk => hash.update(chunk))
        .on('end', () => resolve(hash.digest('hex').toUpperCase()))
        .on('error', e => {
          Logger.error(e, 'GetHashOfFileUtil');
          reject(e);
        });
    });
  }

  static async moveFile(oldPath: string, newPath: string) {
    return new Promise((resolve, reject) => {
      fs.rename(oldPath, newPath, err => {
        if (err) reject(err);
        resolve(true);
      });
    });
  }
}

export class AudioUtils {
  static getMetadata(filepath: string): Promise<ffmpeg.FfprobeData> {
    return new Promise<ffmpeg.FfprobeData>((resolve, reject) => {
      ffmpeg.ffprobe(filepath, (err, data) => {
        if (err) {
          reject(err);
        }
        resolve(data);
      });
    });
  }
}

export class VideoUtils {
  /**
   * 视频截取缩略图
   * @param filepath 视频文件存储路径
   * @param thumbnailConfig `fluent-ffmpeg`的`ScreenshotsConfig`
   */
  static async getThumbs(
    filepath: string,
    thumbnailConfig: ffmpeg.ScreenshotsConfig = {
      count: 1,
      size: '320x240',
      filename: uuidv1() + '.png',
      folder: FileStoragePath.UPLOAD_THUMBS,
    },
  ): Promise<{ filename: string; folder: string; fullpath: string }> {
    return new Promise((resolve, reject) => {
      ffmpeg(filepath)
        .on('error', err => {
          reject(err);
        })
        .on('end', () => {
          const { filename, folder } = thumbnailConfig;
          resolve({
            filename,
            folder,
            fullpath: thumbnailConfig.folder + thumbnailConfig.filename,
          });
        })
        .screenshots(thumbnailConfig);
    });
  }
}

export class PDFUtils {
  /**
   * 生成并保存 PDF 文件的缩略图
   * @param filepath PDF 文件路径
   * @param resizeOption 文件大小调整参数
   */
  static async getThumbOfPDF(
    filepath: string,
    resizeOption: sharp.ResizeOptions = { width: 250 },
  ) {
    const path = FileStoragePath.UPLOAD_THUMBS + uuidv1() + '.png';
    const thumbInfo = await sharp(filepath)
      .png()
      .resize(resizeOption)
      .toFile(path);
    return { path, info: thumbInfo };
  }
}
